# -*- coding: utf-8 -*-
"""
Copyright (c) 2024, XueJunJie
All rights reserved.

This file is part of ChromeBrowClass.

ChromeBrowClass is free software: you can redistribute it and/or modify
it under the terms of the EPL, version 2.0,
as published by the Free Software Foundation.

ChromeBrowClass is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
EPL for the specific language governing permissions and
limitations under the license.

Contact information:

- Email: 609433259@qq.com

- gitee: https://gitee.com/kid0/ChromeBrowClass
"""
"""
chrome浏览器控制类,基于selenium框架二次包装
@author:   XueJunJie
@file:     BrowClass.py
@date:     2024-11-11
@version:  3.2.1
"""
import os
import re
import time
import json
import base64
import platform
import warnings
import traceback
from typing import List, Dict, Set, Any, Optional, Union
from selenium.webdriver import Chrome, ChromeOptions, ActionChains
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import TimeoutException, WebDriverException, SessionNotCreatedException

from . import _f_selenium_version_, _s_selenium_version_
from .BrowSettings.BrowInfoFetch import fetch_user_data_dir
from .BrowSettings.BrowSettings import DriverSettings
from .BrowTools.ProxyauthExtension import create_proxyauth_extension
from .BrowTools.JsCommand import JsScript
from .BrowTools.SystemCommand import (
    get_chrome_version_by_error,
    update_chromedriver,
    update_chromedriver_by_selenium_manager,
    check_chrome_open_port,
    startup_chrome_no_driver,
    kill_chrome,
    kill_chrome_driver,
    not_show_session_recovery,
)

"""
init_web                    启动web
_init_web_runner            启动web具体逻辑
set_driver                  设置driver以控制外部driver
_make_chrome_options        制作配置类object
set_pg_timeout              设置超时
get_capabe                  获取浏览器能力表
get_debug_addr              获取debug调试地址
get_version_message         获取浏览器和driver版本
scroll                      滑条下拉
close                       关闭页面
quit                        关闭浏览器
wait_elm                    【停用】尝试查找元素
kill_other_windows          关闭其他窗口
get                         尝试跳转url
get_cookies                 获取所有cookies
set_cookies                 设置cookies
clear_cookies               清空cookies
get_in_new_window           在新窗口中打开
wait_for_visible            等待元素出现，超时停止加载返回None
wait_for_visible_any        等待多个元素出现，超时停止加载返回None
wait_for_invisible          等待元素消失，超时停止加载返回None
wait_for_clickable          等待元素可点击，超时停止加载返回None
wait_for_presence           等待元素存在，超时停止加载返回None
wait_for_presence_all       等待多个元素存在，超时停止加载返回None
chrome_devtools_order       执行CDT命令
save_as_png                 截图，可用自带函数，也可利用CDT截图，截图可以定义窗口大小，默认可视区域
save_as_png_2               [新版]截图，支持元素/窗口/全页截图
fullweb_show                全页显示
execute_script              执行js语句
expand_shadow_element       shadow-root之下的定位
js_click_by_xpath           根据xpath,用js执行点击，点击click无法点击的元素
js_click_by_element         将elemen用js执行点击，点击click无法点击的元素
scroll_into_view_by_xpath   根据xpath显示元素到当前可视区域
scroll_into_view_by_element 将element显示到当前可视区域
order_to_xpath              根据xpath识别元素，并发送命令
order_to_element            向element发送命令
scroll_by                   视图偏移
post                        在页面post数据
post2                       在页面post数据2版本
"""


class ChromeBrowClass(DriverSettings):
    """
    chrome控制器
    参数参考DriverSettings定义
    """

    def __init__(
        self,
        chromepath: str = "auto",
        driverpath: str = "chromedriver.exe",
        *args,
        proxy: str = "",
        ua: str = "",
        img: int = 0,
        headers: dict = {},
        use_user_data: bool = False,
        user_data_path: str = "",
        sandbox: bool = True,
        gpu: bool = True,
        handless: bool = False,
        slink: bool = False,
        mask: bool = False,
        debugger_address: str = "",
        chrome_arguments: list = [],
        chrome_experimental_options: dict = {},
        crx_extensions: list = [],
        optimize_setting: bool = True,
        log_path: str = None,
        custom_chrome_options: object = None,
        auto_update_chromedriver: bool = False,
        chrome_version: str = "",
        my_update: bool = False,
        chromedriver_download_proxy: dict = None,
        **kwargs,
    ):
        """
        标准初始化
        参数:
            chromepath                      str     'auto'              chrome的路径[不填则使用系统配置,'auto'则尝试自动探寻,其他直接作为参数]
            driverpath                      str     'chromedriver.exe'  chromedriver的路径[默认是运行目录下]
            *
            proxy                           str     ''                  代理 username:password@123.123.123.123:6666 或 123.123.123.123:6666
            ua                              str     ''                  自定义ua
            img                             int     0                   是否加载图片，0默认 1加载 2不加载
            headers                         dist    {}                  请求头
            use_user_data                   bool    False               是否使用用户数据
            user_data_path                  str     ''                  用户数据路径，use_user_data为True才有效
            sandbox                         bool    True                使用沙盒环境，linux无法启动chrome时应该禁止
            gpu                             bool    True                硬件加速，禁用后能减少chrome的内存占用，和加快加载速度
            handless                        bool    False               隐藏窗口，使用无窗口模式记得kill掉chrome或者使用driver.close()
            *slink                          bool    False               不提示“正在自动测试”，默认为False。在部分机器启用会导致浏览器崩溃
            mask                            bool    False               简易伪装不是driver。推荐使用
            debugger_address                str     ''                  连接chrome的调试接口地址。如"localhost:9222"。
                                                                        chrome启动时需要参数"--remote-debugging-port=9222"指定调试端口
            chrome_arguments                list    []                  chrome其他配置参数，例如["--no-sandbox", "--headless"]
            chrome_experimental_options     dict    {}                  chrome实验性配置参数，例如{"excludeSwitches": ["enable-automation"]}
            crx_extensions                  list    []                  插件路径列表
            optimize_setting                bool    True                优化chrome的配置，默认开启。非原生chrome浏览器,或使用debugger_address时建议禁止
            log_path                        str     None                server日志目录，默认没有
            ---其他参数---
            custom_chrome_options           object  None                自定义配置对象。此项非None时，除路径外其他参数无效。
            auto_update_chromedriver        bool    False               自动更新driver
            chrome_version                  str     ''                  指定chrome版本,其他基于chrome底层的浏览器需要指定
            ---特殊参数---
            my_update                       bool    False               使用自定义包装更新driver逻辑
            chromedriver_download_proxy     dict    None                下载chromedriver时使用的代理
        返回：
            无
        """
        super().__init__(
            chromepath=chromepath,
            driverpath=driverpath,
            *args,
            proxy=proxy,
            ua=ua,
            img=img,
            headers=headers,
            use_user_data=use_user_data,
            user_data_path=user_data_path,
            sandbox=sandbox,
            gpu=gpu,
            handless=handless,
            slink=slink,
            mask=mask,
            debugger_address=debugger_address,
            chrome_arguments=chrome_arguments,
            chrome_experimental_options=chrome_experimental_options,
            crx_extensions=crx_extensions,
            optimize_setting=optimize_setting,
            log_path=log_path,
            custom_chrome_options=custom_chrome_options,
            auto_update_chromedriver=auto_update_chromedriver,
            chrome_version=chrome_version,
            my_update=my_update,
            chromedriver_download_proxy=chromedriver_download_proxy,
            **kwargs,
        )
        self._initialize()

    def _initialize(self):
        self.driver = None
        self.actions = None

    def _update_function(self) -> Optional[str]:
        """
        更新driver方法
        res:
            str/None      driver文件路径
        """
        if _f_selenium_version_ >= 4 and _s_selenium_version_ >= 7 and not self.my_update:
            res = update_chromedriver_by_selenium_manager(self.driverpath, browser_path=self.chromepath, proxy=self.chromedriver_download_proxy)
        else:
            res = update_chromedriver(
                "auto" if not self.chromepath else self.chromepath, self.driverpath, self.chrome_version, proxy=self.chromedriver_download_proxy
            )
        return res

    def init_web(self, *args, **kwargs):
        """
        初始化浏览器
        """
        if self.auto_update_chromedriver:
            self._update_function()
        _except_flag = 0
        for _ in range(8):
            try:
                return self._init_web_runner()
            except (WebDriverException, SessionNotCreatedException) as e:
                if "cannot find Chrome binary" in str(e):
                    print("[ERROR]无法连接chrome启动器")
                    if _except_flag & (1 << 2):
                        raise e
                    _except_flag |= 1 << 2
                    continue
                elif "cannot connect to chrome at" in str(e) and self.debugger_address:
                    print("[ERROR]无法连接指定调试地址")
                    if _except_flag & (1 << 3):
                        raise e
                    # chrome出现意料外的异常只有杀掉程序
                    kill_chrome()
                    startup_chrome_no_driver(self.chromepath, str(self._port))
                    _except_flag |= 1 << 3
                    continue
                elif "session deleted because of page crash" in str(e):
                    print("[ERROR]检测到未知错误,尝试重新启动")
                    if _except_flag & (1 << 4):
                        raise e
                    kill_chrome()
                    _except_flag |= 1 << 4
                    continue
                elif (
                    type(e) is SessionNotCreatedException
                    or "needs to be in PATH" in str(e)
                    or "needs to be available in the path" in str(e)
                    or "This version of ChromeDriver only supports Chrome version" in str(e)
                    or "unable to connect to renderer" in str(e)
                    or "ChromeDriver is assuming that Chrome" in str(e)
                    or "obtain driver for chrome" in str(e)
                ):
                    print("[ERROR]检测到chrome和driver版本不匹配")
                    if _except_flag & (1 << 1) or not self.auto_update_chromedriver:
                        raise e
                    else:
                        _chrome_ver_tx = get_chrome_version_by_error(str(e))
                        if _chrome_ver_tx and not self.chrome_version:
                            print(f"chrome版本修正为{_chrome_ver_tx}")
                            self.chrome_version = _chrome_ver_tx
                            self._update_function()
                        _except_flag |= 1 << 1
                        continue
                else:
                    raise e
            except Exception as e:
                raise e

    def _init_web_runner(self) -> Chrome:
        """
        初始化执行者
        """
        # 初始化配置
        if not self.custom_chrome_options:
            self.chrome_options = self._make_chrome_options()
        else:
            self.chrome_options = self.custom_chrome_options
        # 初始化浏览器
        if self._port:
            # 仅对chrome标准进行处理。
            if "chrome.exe" in self._chromepath or "auto" in self._chromepath:
                if not check_chrome_open_port(str(self._port)):
                    kill_chrome()
                    startup_chrome_no_driver(self.chromepath, str(self._port))
        # 启动chrome
        if _f_selenium_version_ >= 4:
            # selenium 4.版的初始化方式
            # 初始化server
            self.server = Service(executable_path=self._driverpath, port=0, service_args=None, log_path=self.log_path, env=None)
            # **初始化chrome类**
            self.driver = Chrome(service=self.server, options=self.chrome_options)
        else:
            # selenium 3.14版的初始化方式
            # **初始化chrome类**
            self.server = None
            self.driver = Chrome(
                executable_path=self._driverpath,
                port=0,
                options=self.chrome_options,
                service_args=None,
                desired_capabilities=None,
                service_log_path=None,
            )
        # 初始化actions类
        self._reset_actions()
        if self.mask:
            #!伪造这不是自动化控制!
            self.driver.execute_cdp_cmd(
                "Page.addScriptToEvaluateOnNewDocument",
                {
                    "source": """
                Object.defineProperty(navigator, 'webdriver', {
                  get: () => undefined
                })
              """
                },
            )
        return self.driver

    def set_driver(self, driver: Chrome) -> bool:
        """设置driver。控制外部已启动的driver"""
        self.driver = driver
        if self.driver:
            self._reset_actions()
        else:
            self.actions = None
        return True

    def _reset_actions(self) -> None:
        """自定义的重构actions【不建议外部调用】"""
        self.actions = ActionChains(self.driver)
        self.actions.reset = self._reset_actions

    def _make_chrome_options(self) -> ChromeOptions:
        """
        制作chrome options
        """
        chrome_options = ChromeOptions()
        # 解除禁止弹窗
        chrome_options.add_argument("--disable-infobars")
        chrome_options.add_argument("--disable-popup-blocking")
        # gpu加速
        if not self.gpu:
            chrome_options.add_argument("--disable-gpu")
        # driver路径
        if not self.driverpath:
            if platform.system().lower() == "linux":
                self.driverpath = "chromedriver"
            elif platform.system().lower() == "windows":
                self.driverpath = "chromedriver.exe"
            else:
                self.driverpath = "chromedriver.exe"
        os.environ["webdriver.chrome.driver"] = self.driverpath
        # chrome的调试端口地址
        if self.debugger_address:
            # 使用端口控制，其他参数无效
            chrome_options.debugger_address = self.debugger_address
        else:
            # 引导chrome路径
            if self.chromepath:
                chrome_options.binary_location = self.chromepath
            # 沙盒模式
            if not self.sandbox:
                chrome_options.add_argument("--no-sandbox")
            # 无头模式
            if self.handless:
                chrome_options.add_argument("--headless")
            # user-agent伪装
            if self.ua:
                chrome_options.add_argument(f'user-agent="{self.ua}"')
            # 代理
            if self.proxy:
                if not re.findall("([\s|\d|\.]*:[\S|\d|\.]*@[\S|\d|\.]*:[\S|\d|\.]*)", self.proxy):
                    chrome_options.add_argument(f"--proxy-server=http://{self.proxy}")
                else:
                    # 使用格式xxx:xxx@123.123.123.123:6666
                    l = self.proxy.split("@")
                    user, password = l[0].split(":")
                    ip, port = l[1].split(":")
                    proxypath = create_proxyauth_extension(ip, port, user, password)
                    chrome_options.add_extension(proxypath)
            # 加载图片
            if self.img:
                # 0 默认
                # 1 使用
                # 2 不使用
                if self.img == 1:
                    chrome_options.add_argument("--blink-settings=imagesEnabled=true")
                else:
                    chrome_options.add_argument("--blink-settings=imagesEnabled=false")
            # 伪装浏览器头
            if self.headers:
                for key, value in self.headers.items():
                    chrome_options.add_argument(f"{key}={value}")
            # 用户数据目录
            if self.use_user_data:
                if not self.user_data_path:
                    if "window" in platform.system().lower():
                        self.user_data_path = fetch_user_data_dir()
                    else:
                        pass
                chrome_options.add_argument(rf"user-data-dir={self.user_data_path}")
            # 伪装浏览器
            if self.mask:
                # 禁用启用Blink运行时的功能(88版本后需要)
                chrome_options.add_argument("--disable-blink-features=AutomationControlled")
                # ***伪装chrome正常启动***
                chrome_options.add_argument("--test-type")
                chrome_options.add_argument("--no-default-browser-check")
                chrome_options.add_argument("--no-first-run")
                # 伪装浏览器是正常退出的
                not_show_session_recovery(self.user_data_path)
            # 不提示弹窗（例如“正在受自动化控制”）[!这东西在部分机器有bug，不默认启动]
            if self.slink:
                if self.debugger_address:
                    warnings.warn("警告！设置了debugger_address不应开启slink")
                chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
                chrome_options.add_experimental_option("useAutomationExtension", False)
            # chrome参数
            if self.chrome_arguments:
                for argument in self.chrome_arguments:
                    chrome_options.add_argument(argument)
            # chrome实验参数
            if self.chrome_experimental_options:
                for _key, value in self.chrome_experimental_options.items():
                    chrome_options.add_experimental_option(_key, value)
            # chrome插件
            if self.crx_extensions:
                for crx in self.crx_extensions:
                    chrome_options.add_extension(crx)
            # 优化配置
            # 使用debugger_address时不要开启此项
            if self.optimize_setting:
                if self._debugger_address:
                    warnings.warn("警告！设置了debugger_address不应开启optimize_setting")
                # 设置为开发者模式
                chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
                # 如果same-site是1 则无法addcookies （没有此项则无法添加cookies）
                experimentalFlags = [
                    "same-site-by-default-cookies@2",
                    "cookies-without-same-site-must-be-secure@2",
                ]
                chromeLocalStatePrefs = {"browser.enabled_labs_experiments": experimentalFlags}
                chrome_options.add_experimental_option("localState", chromeLocalStatePrefs)
                # 不提示 禁用“尝试下载多个文件”的提示（若没有此项，下载超过5个文件会被阻止）
                prefs = {"profile.default_content_setting_values.automatic_downloads": 1}
                chrome_options.add_experimental_option("prefs", prefs)
                # 日志提示等级
                chrome_options.add_argument("--log-level=3")
                # 允许浏览器重定向，Framebusting requires same-origin or a user gesture
                chrome_options.add_argument("disable-web-security")
        return chrome_options

    def set_pg_timeout(self, implicitly: int = None, page_load: int = None, script: int = None) -> bool:
        """
        设置超时,作用在全局。
        参数:
            implicitly   int   [系统默认0]隐式等待最大时间，涉及等待元素的时长
            page_load    int   [系统默认300]页面加载最大时间
            script       int   [系统默认300]js脚本最大加载时间，与page_load类似
        返回：
            bool   设置是否成功
        """
        if not implicitly is None:
            self.driver.implicitly_wait(implicitly)
        if not page_load is None:
            self.driver.set_page_load_timeout(page_load)
        if not script is None:
            self.driver.set_script_timeout(script)
        return True

    def get_capabe(self) -> dict:
        """浏览器能力表"""
        return self.driver.capabilities

    def get_debug_addr(self) -> str:
        """当前调试端口"""
        return self.driver.capabilities["goog:chromeOptions"]["debuggerAddress"]

    def get_version_message(self) -> Set[int]:
        """获取浏览器和driver版本，仅80后的原生版本有效"""
        chrome_ver = self.driver.capabilities["browserVersion"]
        chromedriver_text = self.driver.capabilities["chrome"]["chromedriverVersion"]
        chromedriver_ver = chromedriver_text.split(" ")[0]
        return chrome_ver, chromedriver_ver

    def scroll(self, interval=0, num=5000, sleep=0.1) -> bool:
        """
        拉条
        参数:
            interval  int    每次下拉长度
            num       int    直接跳到拉条最下，interval必须为0
            sleep     float  停顿时间
        返回：
            无
        """
        if not interval:
            js = f"var q=document.documentElement.scrollTop={num}"
            self.driver.execute_script(js)
            time.sleep(3)
        else:
            for i in range(200, 4800, interval):
                js = f"var q=document.documentElement.scrollTop={i}"
                self.driver.execute_script(js)
                time.sleep(sleep)
        return True

    def close(self) -> bool:
        """
        关闭浏览器页面
        参数:
            msg   str   关闭信息
        返回：
            bool   关闭结果
        """
        if self.driver:
            self.driver.close()
        return True

    def quit(self) -> bool:
        """
        退出浏览器
        参数:
            msg   str   关闭信息
        返回：
            bool   关闭结果
        """
        if self.driver:
            if self.debugger_address:
                if _f_selenium_version_ >= 4 and _s_selenium_version_ >= 7:
                    self.driver.service.stop()
                else:
                    pass
            else:
                self.driver.quit()
            self.driver = None
            self.actions = None
        return True

    def wait_elm(self, xpath: str, waittime=10, trytime=3) -> Optional[WebElement]:
        """
        等待元素出现（仅支持xpath）
        参数:
            xpath      str  必有   xpath
            waittime   int         等待时间
            trytime    int         最大尝试次数
        返回：
            None/elm       对应元素
        """
        warnings.warn("请使用self.wait_for_presence代替此函数以获取更好的体验。此函数不再维护")
        for i in range(trytime - 1, -1, -1):
            try:
                elm = self.driver.find_element(By.XPATH, xpath)
                return elm
            except Exception as e:
                if not i:
                    break
                else:
                    time.sleep(waittime)
        return None

    def kill_other_windows(self) -> bool:
        """
        关闭除当前窗口外的其他窗口
        参数:
            无
        返回：
            bool    关闭完成
        """
        now_handle = self.driver.current_window_handle
        all_handle = self.driver.window_handles
        if len(all_handle) <= 1:
            return True
        for x in all_handle:
            if x != now_handle:
                self.driver.switch_to.window(x)
                self.driver.execute_script("window.close()")
        self.driver.switch_to.window(now_handle)
        return True

    def get(self, url: str, try_time=3) -> bool:
        """
        新式get
        参数:
            url  str   必有字段   链接
        返回：
            bool       是否成功
        """
        for _ in range(try_time - 1, -1, -1):
            try:
                self.driver.get(url)
                return True
            except TimeoutException as e:
                if not try_time:
                    break
                else:
                    print(f"get {url} 超时，重试")
                    time.sleep(10)
                    continue
        return False

    def get_cookies(self) -> List[Dict]:
        """
        获取cookies
        参数:
            无
        返回：
            dict      得到的cookies
        """
        s = self.driver.get_cookies()
        return s

    def set_cookies(self, l: list, domain=True, httpOnly=True, use_js=False) -> bool:
        """
        设置cookies
        参数:
            :l         list/dict/str    cookies字符串
            :domain    bool             是否保持domain设置，默认True
            :httpOnly  bool             是否保持httponly设置，默认true
            :use_js    bool             使用js添加cookies，默认false。原生selenium无法跨域，但js可以
        返回：
            bool     结果
        """
        if type(l) == str:
            l = json.loads(l)
        if type(l) == dict:
            for k, v in l.items():
                x = {"name": k, "value": v}
                self.driver.add_cookie(x)
        elif type(l) == list:
            for x in l:
                if "sameSite" in x:
                    x.pop("sameSite")
                if "expiry" in x:
                    x.pop("expiry")
                for k, v in x.items():
                    if type(v) == str and k not in (
                        "name",
                        "value",
                    ):
                        if v.lower() == "false":
                            x[k] = False
                        elif v.lower() == "true":
                            x[k] = True
                if not use_js:
                    self.driver.add_cookie(x)
                else:
                    _order = ";".join([f"{k}:{v}" for k, v in x.items()])
                    order = f"document.cookie='{_order}'"
                    self.driver.execute_script(order)
        return True

    def clear_cookies(self) -> bool:
        """
        清空cookies
        参数:
            无
        返回：
            bool       是否成功
        """
        self.driver.delete_all_cookies()
        return True

    def get_in_new_window(self, url: str) -> bool:
        """
        在新窗口打开url
        参数:
            url  str   必有字段   链接
        返回：
            bool       是否成功
        """
        js = f'window.open("{url}");'
        old_handles = self.driver.window_handles
        self.driver.execute_script(js)
        handles = self.driver.window_handles  # 获取当前窗口句柄集合（列表类型）
        for handle in handles[::-1]:  # 切换窗口
            if handle not in old_handles:
                self.driver.switch_to.window(handle)
                break
        return True

    def _my_wait_function(self, function: object, xpath: str, timeout=10, type=By.XPATH, root=None) -> object:
        """
        【禁止外部调用】自定义调用等待
        参数:
            function  function      *元素等待函数
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        res:
            any/None          函数结果
        """
        if root is None:
            root = self.driver
        try:
            element = WebDriverWait(root, timeout).until(function((type, xpath)))
        except TimeoutException as exc:
            return None
        return element

    def wait_for_visible(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> Optional[WebElement]:
        """
        等待元素可视，只匹配首个符合项
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            elm/none       查到的元素
        """
        return self._my_wait_function(EC.visibility_of_element_located, xpath, timeout, type, root)

    def wait_for_visible_any(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> List[WebElement]:
        """
        等待任意元素可视，多个元素中符合"可视"的元素列表
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            list[elm]          查到的元素
        """
        return self._my_wait_function(EC.visibility_of_element_located, xpath, timeout, type, root)

    def wait_for_invisible(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> Optional[WebElement]:
        """
        等待元素消失不可见
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            elm/False       查到的元素
        """
        return self._my_wait_function(EC.invisibility_of_element_located, xpath, timeout, type, root)

    def wait_for_clickable(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> Optional[WebElement]:
        """
        等待元素可点击
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            elm/none       查到的元素
        """
        return self._my_wait_function(EC.element_to_be_clickable, xpath, timeout, type, root)

    def wait_for_presence(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> Optional[WebElement]:
        """
        等待元素存在
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            elm/none       查到的元素
        """
        return self._my_wait_function(EC.presence_of_element_located, xpath, timeout, type, root)

    def wait_for_presence_all(self, xpath: str, timeout=10, type=By.XPATH, root=None) -> List[WebElement]:
        """
        等待至少一个元素存在
        参数:
            xpath     str           xpath
            timeout   int           最大等待时间
            type      str           匹配类型，支持xpath、css等
            root      element/None  基于父元素，None则默认driver
        返回：
            list[elm]        查到的元素
        """
        return self._my_wait_function(EC.presence_of_all_elements_located, xpath, timeout, type, root)

    def chrome_devtools_order(self, order: str, **kwarg) -> str:
        """
        向chrome devtool工具发送命令。
        传入：
            cmd     str     命令
            kwarg   dist    其他参数
        返回：
            api的返回
        """
        resource = rf"/session/{self.driver.session_id}/chromium/send_command_and_get_result"
        if hasattr(self.driver.command_executor, "_url"):
            base_url = self.driver.command_executor._url
        else:
            base_url = self.driver.command_executor._client_config.remote_server_addr
        url = base_url + resource
        body = json.dumps({"cmd": order, "params": kwarg})
        response = self.driver.command_executor._request("POST", url, body)
        if "status" in resource and response["status"]:
            raise Exception(response.get("value"))
        return response.get("value")

    def save_as_png(
        self,
        filepath="html.png",
        view_width=0,
        view_height=0,
        least_height=900,
        least_width=1600,
        max_height=9999,
        max_width=1920,
        root: WebElement = None,
    ) -> str:
        """
        保存图片使用指定的视图大小。0,0 则是当前视图大小；-1，-1则自动全屏截图
        传入：
            filepath         str           保存为图片的文件路劲
            view_width       int           视图宽度，决定截图大小
            view_height      int           视图高度，决定截图大小
            least_height     int           最少高度，height -1时生效
            least_width      int           最少宽度，width -1时生效
            max_height       int           最大高度，height -1时生效
            max_width        int           最大宽度，width -1时生效
            root             WebElement    截图指定元素，默认是指body
        返回：
            str/None         文件路径
        """
        if view_height and view_width:
            if view_height == -1 or view_width == -1:
                if root:
                    size = root.size
                else:
                    size = self.driver.find_element(By.XPATH, "//body").size
                if view_height == -1:
                    view_height = size["height"] if size["height"] > least_height else least_height
                    view_height = view_height if view_height < max_height else max_height
                if view_width == -1:
                    view_width = size["width"] if size["width"] > least_width else least_width
                    view_width = view_width if view_width < max_width else max_width
            self.chrome_devtools_order("Emulation.setVisibleSize", width=view_width, height=view_height)
            print(f"change web window size as {view_width}:{view_height}")
            data = self.chrome_devtools_order("Page.captureScreenshot", format="png", fromSurface=True)
            img = data["data"]
            data2 = base64.b64decode(img)
            with open(filepath, "wb") as f:
                f.write(data2)
                f.close()
            return filepath
        else:
            # 自带截图函数
            if root:
                if root.screenshot(filepath):
                    return filepath
                else:
                    return None
            else:
                if self.driver.save_screenshot(filepath):
                    return filepath
                else:
                    return None

    def save_as_png_2(
        self,
        filepath="html.png",
        view_width=0,
        view_height=0,
        least_height=900,
        least_width=1600,
        max_height=9999,
        max_width=1920,
        root: WebElement = None,
    ) -> str:
        """
        【新版】保存图片使用指定的视图大小。0,0 则是当前视图大小；-1，-1则自动全屏截图
        传入：
            filepath         str           保存为图片的文件路劲
            view_width       int           视图宽度，决定截图大小
            view_height      int           视图高度，决定截图大小
            least_height     int           最少高度，height -1时生效
            least_width      int           最少宽度，width -1时生效
            max_height       int           最大高度，height -1时生效
            max_width        int           最大宽度，width -1时生效
            root             WebElement    截图指定元素，默认是指body
        返回：
            str/None         文件路径
        """
        if root:
            # 元素截图
            return root.screenshot(filepath)
        if not view_width or not view_height:
            # 自带截图函数
            return self.driver.get_screenshot_as_file(filepath)
        if view_height and view_width:
            # 自定义尺寸
            now_size = self.driver.get_window_size()
            now_width = now_size["width"]
            now_height = now_size["height"]
            body_width = self.driver.execute_script("return document.body.scrollWidth")
            body_height = self.driver.execute_script("return document.body.scrollHeight")
            if view_height == -1:
                view_height = body_height
            elif view_height:
                view_height = view_height if view_height < max_height else max_height
                view_height = view_height if view_height > least_height else least_height
            if view_width == -1:
                view_width = body_width
            elif view_width:
                view_width = view_width if view_width < max_width else max_width
                view_width = view_width if view_width > least_width else least_width
            # 截图
            # self.driver.set_window_size(view_width, view_height)
            self.fullweb_show(view_height, view_width)
            res = self.driver.get_screenshot_as_file(filepath)
            # 恢复
            # self.driver.set_window_size(now_width, now_height)
            self.fullweb_show(now_height, now_width)
            return res
        raise Exception("参数错误")

    def fullweb_show(self, height=-1, width=-1, _xpath="//html") -> bool:
        """
        全页显示。与窗口最大化不同，显示大小以页面大小为准。
        :height     int     高度
        :width      int     宽度
        :_xpath     str     页面root的xpath，默认html
        """
        size = self.driver.find_element(By.XPATH, _xpath).size
        if height == -1:
            height = size["height"]
        if width == -1:
            width = size["width"]
        self.chrome_devtools_order("Emulation.setVisibleSize", width=width, height=height)
        return True

    def execute_script(self, script: str) -> Any:
        """
        执行js代码
        传入:
            script      str      js代码
        返回：
            运行结果返回
        """
        return self.driver.execute_script(script)

    def expand_shadow_element(self, element: WebElement) -> Any:
        """
        shadow应对；定位shadow下的节点。
        第一层可以用xpath，之下的定位只能用find_element的css_selector定位,xpath会报错
        """
        shadow_root = self.driver.execute_script("return arguments[0].shadowRoot", element)
        return shadow_root

    def js_click_by_xpath(self, xpath: str, i=0) -> Any:
        """
        用js方法点击。用于click点击的元素
        """
        script = JsScript.findElementByXpath + """getElementsByXpath('{}')[{}].click();""".format(xpath.replace("'", "\\'"), i)
        return self.driver.execute_script(script)

    def js_click_by_element(self, element: WebElement) -> Any:
        """
        用js方法点击。用于click点击的元素
        """
        return self.driver.execute_script("return arguments[0].click();", element)

    def scroll_into_view_by_xpath(self, xpath: str, i=0, dx=0, dy=0) -> Any:
        """
        用js方法把元素显示在当前可视区域
        :xpath      str       xpath定位
        :i          int       元素序号，默认0
        :dx         int       x轴偏移量
        :dy         int       y轴偏移量
        """
        script = (
            JsScript.findElementByXpath
            + """
            elm = getElementsByXpath('{}')[{}];
            res = elm.scrollIntoView();
        """.format(
                xpath.replace("'", "\\'"), i
            )
        )
        if dx or dy:
            script += f"window.scrollBy({dx}, {dy});"
        script += "return res;"
        return self.driver.execute_script(script)

    def scroll_into_view_by_element(self, element: WebElement, dx=0, dy=0) -> Any:
        """
        用js方法把元素显示在当前可视区域
        :element    WebElement   指定元素
        :dx         int          x轴偏移量
        :dy         int          y轴偏移量
        """
        script = "res = arguments[0].scrollIntoView();"
        if dx or dy:
            script += f"window.scrollBy({dx}, {dy});"
        script += "return res;"
        return self.driver.execute_script(script, element)

    def order_to_xpath(self, xpath: str, order: str, i=0) -> Any:
        """
        xpath识别元素,并执行order命令
        """
        script = JsScript.findElementByXpath + """return getElementsByXpath('{}')[{}].{};""".format(xpath.replace("'", "\\'"), i, order)
        return self.driver.execute_script(script)

    def order_to_element(self, element: object, order: str) -> Any:
        """
        对指定元素发送js命令。一般shadow应对，正常情况也有效
        :element    WebElement   指定元素
        :order      str          命令
        """
        script = "return arguments[0].{};".format(order.replace("'", "\\'"))
        res = self.driver.execute_script(script, element)
        return res

    def scroll_by(self, dx=0, dy=0):
        """
        视图偏移
        :dx     int      x轴偏移量
        :dy     int      y轴偏移量
        """
        script = f"window.scrollBy({dx}, {dy});"
        res = self.driver.execute_script(script)
        return res

    def post(self, url: str, params={}) -> bool:
        """
        在页面post（注意，此请求会导致页面转跳。建议执行完后返回页面 或在新标签执行）
        """
        for key, value in params.items():
            if type(value) == bool:
                params[key] = str(value).lower()
        script = JsScript.webHttpPost + f"""httpPost('{url}', {params});"""
        self.driver.execute_script(script)
        return True

    def post2(self, url: str, data: dict) -> str:
        """
        发送post数据到指定url,基于浏览器页面
        """
        json_data = json.dumps(data)
        order = f"""
return new Promise((resolve, reject) => {{
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "{url}", true);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.onreadystatechange = function () {{
        if (xhr.readyState === 4) {{
            if (xhr.status === 200) {{
                resolve(xhr.responseText);  // 解析响应
            }} else {{
                reject('请求失败，状态码：' + xhr.status);
            }}
        }}
    }};
    xhr.send('{json_data}');
}});
"""
        res = self.driver.execute_script(order)
        return res


if __name__ == "__main__":
    C = ChromeBrowClass(auto_update_chromedriver=True)
    C.init_web()
    C.get("https://www.baidu.com")
    input("输入任意键继续...")
    C.quit()
